/*
 * Copyright (c) 2011 Sultan Rehman
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package net.rehman.fbauth;

import java.io.IOException;
import java.io.Writer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.codehaus.jackson.map.ObjectMapper;

/**
 * Contains methods to make Facebook authorization easier
 *
 * Adapted from code posted in http://www.hammersoft.de/blog/?p=87
 *
 * @author sultanr
 */
public class FbAuthHelper {

    public static final String FACEBOOK_AUTHORIZE_URL_BASE = "https://graph.facebook.com/oauth/authorize?";

    private static final Logger LOG = LogManager.getLogger( FbAuthHelper.class );

    private final String appId;
    private final String appSecret;
    private final String appExtPerms;

    public FbAuthHelper( final String appId, final String appSecret ) {
        this( appId, appSecret, null );
    }

    public FbAuthHelper( final String appId, final String appSecret, final String appExtPerms ) {
        if ( appId == null ) {
            throw new IllegalArgumentException( "Parameter 'appId' cannot be null" );
        }
        if ( appSecret == null ) {
            throw new IllegalArgumentException( "Parameter 'appSecret' cannot be null" );
        }
        this.appId = appId;
        this.appSecret = appSecret;
        this.appExtPerms = appExtPerms;
    }

    /**
     * Constructs a Facebook application authorization URL string
     *
     * @param redirectUrl URL to redirect to after authorization
     * @return constructed URL
     */
    public String buildFacebookAuthorizeUrl( final String redirectUrl ) {
        if ( redirectUrl == null ) {
            throw new IllegalArgumentException( "Parameter 'redirectUrl' cannot be null" );
        }
        StringBuilder s = new StringBuilder( FACEBOOK_AUTHORIZE_URL_BASE ).
                                     append( "client_id=" ).append( appId );
        if ( appExtPerms != null && appExtPerms.length() > 0 ) {
            s.append( "&scope=" ).append( appExtPerms );
        }
        if ( redirectUrl != null ) {
            s.append( "&redirect_uri=" ).append( redirectUrl );
        }
        return ( s.toString() );
   }

    /**
     * Parses and verifies Facebook 'signed_request' parameter.
     *
     * @param sigreq signed_request parameter
     * @return Map of attributes from parsed signed_request
     * @throws IOException
     */
    public Map<String, Object> parseSignedRequest( final String sigreq ) throws IOException {
        if ( sigreq == null ) {
            throw new IllegalArgumentException( "Parameter 'sigreq' cannot be null" );
        }
        int idx = sigreq.indexOf( "." );
        if ( idx < 0 ) {
            throw new IOException( "Invalid format in 'signed_request'" );
        }
        byte[] sig = new Base64( true ).decode( sigreq.substring( 0, idx ).getBytes());
        String rawpayload = sigreq.substring( idx + 1 );
        String payload = new String( new Base64( true ).decode( rawpayload ) );
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> map = mapper.readValue( payload, HashMap.class );
        if ( !map.containsKey( "algorithm" ) || !map.get( "algorithm" ).equals( "HMAC-SHA256" ) ) {
          throw new IOException( "Unexpected hash algorithm encountered: algorithm=" + map.get( "algorithm" ) );
        }
        checkSignature( rawpayload, sig );
        return ( map );
    }

    /**
     * Verifies session is authorized and not expired
     *
     * @param sigreq Map containing signed_request
     * @return TRUE if session is authorized and not expired
     */
    public boolean isAuthorized( final Map<String, Object> sigreq ) {
        if ( sigreq == null ) {
            throw new IllegalArgumentException( "Parameter 'sigreq' cannot be null" );
        }
        boolean result = false;
        if ( sigreq.containsKey( "user_id" ) ) {
            if ( sigreq.containsKey( "expires" ) ) {
                long expires = ((Integer)sigreq.get( "expires" )).longValue();
                long now = System.currentTimeMillis() / 1000L;
                if ( expires <= 0 || expires > now ) {
                    result = true;
                }
                else {
                    if ( LOG.isDebugEnabled() ) {
                        LOG.debug( "Session has expired: expires=" + expires + ", now=" + now );
                    }
                }
            }
            else {
                if ( LOG.isDebugEnabled() ) {
                    LOG.debug( "'signed_request' has no 'expires' field so assume this session never expires" );
                }
                result = true;
            }
        }
        return ( result );
    }

    /**
     * Returns HTML for redirecting to Facebook authorization URL.
     * 
     * @param out
     * @param redirectUrl
     * @throws IOException
     */
    public String getAuthRedirectHtml( final String redirectUrl ) throws IOException {
        String authUrl = buildFacebookAuthorizeUrl( redirectUrl );
        StringBuilder s = new StringBuilder().
                                append( "<html><head><script type=\"text/javascript\">window.top.location = \"" ).
                                append( authUrl ).
                                append( "\";</script></head><body><noscript>\n" ).
                                append( "You must <a href=\"" ).
                                append( authUrl ).
                                append( "\">authorize the application</a> in order to continue.</noscript></body></html>" );
        return ( s.toString() );
    }

     /**
     * Writes HTML for redirecting to Facebook authorization URL to a Writer.
     *
     * @param out
     * @param redirectUrl
     * @throws IOException
     */
    public void writeAuthRedirectHtml( final Writer out, final String redirectUrl ) throws IOException {
        out.write( getAuthRedirectHtml( redirectUrl ) );
    }
    
    public String getAppExtPerms() {
        return appExtPerms;
    }

    public String getAppId() {
        return appId;
    }

    public String getAppSecret() {
        return appSecret;
    }

    private static final String SIGN_ALGORITHM = "HMACSHA256";

    private void checkSignature( String rawpayload, byte[] sig ) throws IOException {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec( appSecret.getBytes(), SIGN_ALGORITHM );
            Mac mac = Mac.getInstance( SIGN_ALGORITHM );
            mac.init(secretKeySpec);
            byte[] mysig = mac.doFinal( rawpayload.getBytes() );
            if ( !Arrays.equals( mysig, sig ) ) {
		throw new IOException( "Non-matching signature for request" );
            }
        }
        catch ( NoSuchAlgorithmException e ) {
            throw new IOException( "Unknown hash algorithm " + SIGN_ALGORITHM, e );
        }
        catch ( InvalidKeyException e ) {
            throw new IOException( "Wrong key for " + SIGN_ALGORITHM, e );
        }
    }
}
